#include "QtConnection.hpp"

#include <cmath>
#include <utility>

#include <QtWidgets/QtWidgets>
#include <QtGlobal>

#include "QtNode.hpp"
#include "QtFlowScene.hpp"
#include "QtFlowView.hpp"

#include "QtNodeGraphicsObject.hpp"
#include "QtNodeDataModel.hpp"
#include "QtConnectionGraphicsObject.hpp"

#include "QtStyle.hpp"


namespace Qt
{
	ConnectionGeometry::
		ConnectionGeometry()
		: _in(0, 0)
		, _out(0, 0)
		//, _animationPhase(0)
		, _lineWidth(3.0)
		, _hovered(false)
	{ }

	QPointF const&
		ConnectionGeometry::
		getEndPoint(PortType portType) const
	{
		Q_ASSERT(portType != PortType::None);

		return (portType == PortType::Out ?
			_out :
			_in);
	}


	void
		ConnectionGeometry::
		setEndPoint(PortType portType, QPointF const& point)
	{
		switch (portType)
		{
		case PortType::Out:
			_out = point;
			break;

		case PortType::In:
			_in = point;
			break;

		default:
			break;
		}
	}


	void
		ConnectionGeometry::
		moveEndPoint(PortType portType, QPointF const &offset)
	{
		switch (portType)
		{
		case PortType::Out:
			_out += offset;
			break;

		case PortType::In:
			_in += offset;
			break;

		default:
			break;
		}
	}


	QRectF
		ConnectionGeometry::
		boundingRect() const
	{
		auto points = pointsC1C2();

		QRectF basicRect = QRectF(_out, _in).normalized();

		QRectF c1c2Rect = QRectF(points.first, points.second).normalized();

		auto const &connectionStyle =
			StyleCollection::connectionStyle();

		float const diam = connectionStyle.pointDiameter();

		QRectF commonRect = basicRect.united(c1c2Rect);

		QPointF const cornerOffset(diam, diam);

		commonRect.setTopLeft(commonRect.topLeft() - cornerOffset);
		commonRect.setBottomRight(commonRect.bottomRight() + 2 * cornerOffset);

		return commonRect;
	}


	std::pair<QPointF, QPointF>
		ConnectionGeometry::
		pointsC1C2() const
	{
		const double defaultOffset = 200;

		double xDistance = _in.x() - _out.x();

		double horizontalOffset = qMin(defaultOffset, std::abs(xDistance));

		double verticalOffset = 0;

		double ratioX = 0.5;

		if (xDistance <= 0)
		{
			double yDistance = _in.y() - _out.y() + 20;

			double vector = yDistance < 0 ? -1.0 : 1.0;

			verticalOffset = qMin(defaultOffset, std::abs(yDistance)) * vector;

			ratioX = 1.0;
		}

		horizontalOffset *= ratioX;

		QPointF c1(_out.x() + horizontalOffset,
			_out.y() + verticalOffset);

		QPointF c2(_in.x() - horizontalOffset,
			_in.y() - verticalOffset);

		return std::make_pair(c1, c2);
	}

	ConnectionState::
		~ConnectionState()
	{
		resetLastHoveredNode();
	}


	void
		ConnectionState::
		interactWithNode(QtNode* node)
	{
		if (node)
		{
			_lastHoveredNode = node;
		}
		else
		{
			resetLastHoveredNode();
		}
	}


	void
		ConnectionState::
		setLastHoveredNode(QtNode* node)
	{
		_lastHoveredNode = node;
	}


	void
		ConnectionState::
		resetLastHoveredNode()
	{
		if (_lastHoveredNode)
			_lastHoveredNode->resetReactionToConnection();

		_lastHoveredNode = nullptr;
	}

	QtConnection::
		QtConnection(PortType portType,
			QtNode& node,
			PortIndex portIndex)
		: _uid(QUuid::createUuid())
		, _outPortIndex(INVALID_PORT)
		, _inPortIndex(INVALID_PORT)
		, _connectionState()
	{
		setNodeToPort(node, portType, portIndex);

		setRequiredPort(oppositePort(portType));
	}


	QtConnection::
		QtConnection(QtNode& nodeIn,
			PortIndex portIndexIn,
			QtNode& nodeOut,
			PortIndex portIndexOut,
			TypeConverter typeConverter)
		: _uid(QUuid::createUuid())
		, _outNode(&nodeOut)
		, _inNode(&nodeIn)
		, _outPortIndex(portIndexOut)
		, _inPortIndex(portIndexIn)
		, _connectionState()
		, _converter(std::move(typeConverter))
	{
		setNodeToPort(nodeIn, PortType::In, portIndexIn);
		setNodeToPort(nodeOut, PortType::Out, portIndexOut);
	}


	QtConnection::
		~QtConnection()
	{
		if (complete())
		{
			connectionMadeIncomplete(*this);
		}

		propagateDisconnectedData();
		//propagateEmptyData();

		if (_inNode)
		{
			_inNode->nodeGraphicsObject().update();
		}

		if (_outNode)
		{
			_outNode->nodeGraphicsObject().update();
		}
	}


	QJsonObject
		QtConnection::
		save() const
	{
		QJsonObject connectionJson;

		if (_inNode && _outNode)
		{
			connectionJson["in_id"] = _inNode->id().toString();
			connectionJson["in_index"] = _inPortIndex;

			connectionJson["out_id"] = _outNode->id().toString();
			connectionJson["out_index"] = _outPortIndex;

			if (_converter)
			{
				auto getTypeJson = [this](PortType type)
				{
					QJsonObject typeJson;
					NodeDataType nodeType = this->dataType(type);
					typeJson["id"] = nodeType.id;
					typeJson["name"] = nodeType.name;

					return typeJson;
				};

				QJsonObject converterTypeJson;

				converterTypeJson["in"] = getTypeJson(PortType::In);
				converterTypeJson["out"] = getTypeJson(PortType::Out);

				connectionJson["converter"] = converterTypeJson;
			}
		}

		return connectionJson;
	}


	QUuid
		QtConnection::
		id() const
	{
		return _uid;
	}


	bool
		QtConnection::
		complete() const
	{
		return _inNode != nullptr && _outNode != nullptr;
	}


	void
		QtConnection::
		setRequiredPort(PortType dragging)
	{
		_connectionState.setRequiredPort(dragging);

		switch (dragging)
		{
		case PortType::Out:
			_outNode = nullptr;
			_outPortIndex = INVALID_PORT;
			break;

		case PortType::In:
			_inNode = nullptr;
			_inPortIndex = INVALID_PORT;
			break;

		default:
			break;
		}
	}


	PortType
		QtConnection::
		requiredPort() const
	{
		return _connectionState.requiredPort();
	}


	void
		QtConnection::
		setGraphicsObject(std::unique_ptr<QtConnectionGraphicsObject>&& graphics)
	{
		_connectionGraphicsObject = std::move(graphics);

		// This function is only called when the ConnectionGraphicsObject
		// is newly created. At this moment both end coordinates are (0, 0)
		// in QtConnection G.O. coordinates. The position of the whole
		// QtConnection G. O. in scene coordinate system is also (0, 0).
		// By moving the whole object to the QtNode Port position
		// we position both connection ends correctly.

		if (requiredPort() != PortType::None)
		{

			PortType attachedPort = oppositePort(requiredPort());

			PortIndex attachedPortIndex = getPortIndex(attachedPort);

			auto node = getNode(attachedPort);

			QTransform nodeSceneTransform =
				node->nodeGraphicsObject().sceneTransform();

			QPointF pos = node->nodeGeometry().portScenePosition(attachedPortIndex,
				attachedPort,
				nodeSceneTransform);

			_connectionGraphicsObject->setPos(pos);
		}

		_connectionGraphicsObject->move();
	}



	PortIndex
		QtConnection::
		getPortIndex(PortType portType) const
	{
		PortIndex result = INVALID_PORT;

		switch (portType)
		{
		case PortType::In:
			result = _inPortIndex;
			break;

		case PortType::Out:
			result = _outPortIndex;

			break;

		default:
			break;
		}

		return result;
	}


	void
		QtConnection::
		setNodeToPort(QtNode& node,
			PortType portType,
			PortIndex portIndex)
	{
		bool wasIncomplete = !complete();

		auto& nodeWeak = getNode(portType);

		nodeWeak = &node;

		if (portType == PortType::Out)
			_outPortIndex = portIndex;
		else
			_inPortIndex = portIndex;

		_connectionState.setNoRequiredPort();

		updated(*this);
		if (complete() && wasIncomplete) {
			connectionCompleted(*this);
		}
	}


	void
		QtConnection::
		removeFromNodes() const
	{
		if (_inNode)
			_inNode->nodeState().eraseConnection(PortType::In, _inPortIndex, id());

		if (_outNode)
			_outNode->nodeState().eraseConnection(PortType::Out, _outPortIndex, id());
	}


	QtConnectionGraphicsObject&
		QtConnection::
		getConnectionGraphicsObject() const
	{
		return *_connectionGraphicsObject;
	}


	ConnectionState&
		QtConnection::
		connectionState()
	{
		return _connectionState;
	}


	ConnectionState const&
		QtConnection::
		connectionState() const
	{
		return _connectionState;
	}


	ConnectionGeometry&
		QtConnection::
		connectionGeometry()
	{
		return _connectionGeometry;
	}


	ConnectionGeometry const&
		QtConnection::
		connectionGeometry() const
	{
		return _connectionGeometry;
	}


	QtNode*
		QtConnection::
		getNode(PortType portType) const
	{
		switch (portType)
		{
		case PortType::In:
			return _inNode;
			break;

		case PortType::Out:
			return _outNode;
			break;

		default:
			// not possible
			break;
		}
		return nullptr;
	}


	QtNode*&
		QtConnection::
		getNode(PortType portType)
	{
		switch (portType)
		{
		case PortType::In:
			return _inNode;
			break;

		case PortType::Out:
			return _outNode;
			break;

		default:
			// not possible
			break;
		}
		Q_UNREACHABLE();
	}


	void
		QtConnection::
		clearNode(PortType portType)
	{
		if (complete())
		{
			connectionMadeIncomplete(*this);
		}

		getNode(portType) = nullptr;

		if (portType == PortType::In)
			_inPortIndex = INVALID_PORT;
		else
			_outPortIndex = INVALID_PORT;
	}


	NodeDataType
		QtConnection::
		dataType(PortType portType) const
	{
		if (_inNode && _outNode)
		{
			auto const & model = (portType == PortType::In) ?
				_inNode->nodeDataModel() :
				_outNode->nodeDataModel();
			PortIndex index = (portType == PortType::In) ?
				_inPortIndex :
				_outPortIndex;

			return model->dataType(portType, index);
		}
		else
		{
			QtNode* validNode;
			PortIndex index = INVALID_PORT;

			if ((validNode = _inNode))
			{
				index = _inPortIndex;
				portType = PortType::In;
			}
			else if ((validNode = _outNode))
			{
				index = _outPortIndex;
				portType = PortType::Out;
			}

			if (validNode)
			{
				auto const &model = validNode->nodeDataModel();

				return model->dataType(portType, index);
			}
		}

		Q_UNREACHABLE();
	}


	void
		QtConnection::
		setTypeConverter(TypeConverter converter)
	{
		_converter = std::move(converter);
	}


	void
		QtConnection::
		propagateData(std::shared_ptr<QtNodeData> nodeData) const
	{
		if (_inNode)
		{
			if (_converter)
			{
				nodeData = _converter(nodeData);
			}

			_inNode->propagateData(nodeData, _inPortIndex);
		}
	}


	void
		QtConnection::
		propagateEmptyData() const
	{
		std::shared_ptr<QtNodeData> emptyData;

		propagateData(emptyData);
	}

	void QtConnection::propagateDisconnectedData() const
	{
		std::shared_ptr<QtNodeData> deletedData = nullptr;
		if (_outNode)
		{
			deletedData = _outNode->nodeDataModel()->outData(_outPortIndex);
			if (_inNode && deletedData) {
				deletedData->setConnectionType(CntType::Break);
			}
		}

		propagateData(deletedData);
	}
}